package org.papiez.guice.extension;

import static net.sf.cglib.core.Constants.CONSTRUCTOR_NAME;
import static net.sf.cglib.core.Constants.SOURCE_FILE;
import static net.sf.cglib.core.ReflectUtils.defineClass;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import net.sf.cglib.asm.ClassReader;
import net.sf.cglib.asm.ClassWriter;
import net.sf.cglib.asm.FieldVisitor;
import net.sf.cglib.asm.Type;
import net.sf.cglib.core.CodeEmitter;
import net.sf.cglib.core.Signature;
import net.sf.cglib.transform.ClassEmitterTransformer;
import net.sf.cglib.transform.ClassTransformer;

import org.apache.commons.lang.Validate;

final class DerivedClassGenerator<T> {
	private final Class<T> superClass;

	private DerivedClassGenerator(Class<T> superClass) {
		Validate.notNull(superClass, "");
		this.superClass = superClass;
	}

	public static <T> DerivedClassGenerator<T> forClass(Class<T> superClass) {
		return new DerivedClassGenerator<T>(superClass);
	}

	public <U> Class<? extends T> narrowConstructorsParameterType(
			String derivedClassName, Class<U> parameterClass,
			Class<? extends U> narrowedParameterClass) {

		Map<Class<?>, Class<?>> narrowingMap = new HashMap<Class<?>, Class<?>>();
		narrowingMap.put(parameterClass, narrowedParameterClass);

		return narrowConstructorsParameterTypes(derivedClassName, narrowingMap);
	}

	public Class<? extends T> narrowConstructorsParameterTypes(
			String derivedClassName, Map<Class<?>, Class<?>> narrowingMap) {

		Validate.notEmpty(derivedClassName, "");
		validate(narrowingMap);

		try {
			return castSafely(superClass.getClassLoader().loadClass(
					derivedClassName));
		} catch (ClassNotFoundException e) {
		}

		ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);

		ClassTransformer transformer = new ConstructorsParameterTypesNarrowingTransformer(
				derivedClassName,
				buildParameterTypesNarrowingRules(narrowingMap));
		transformer.setTarget(writer);

		try {
			String packageString = "";
			if (superClass.getPackage() != null) {
				packageString = superClass.getPackage().getName() + ".";
			}

			InputStream superClassSourceStream = superClass
					.getResourceAsStream(superClass.getName().replace(
							packageString, "")
							+ ".class");

			new ClassReader(superClassSourceStream).accept(transformer,
					ClassReader.SKIP_CODE);
		} catch (IOException e) {
			throw new IllegalStateException("", e);
		}

		try {
			return castSafely(defineClass(derivedClassName,
					writer.toByteArray(), superClass.getClassLoader()));
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			throw new IllegalStateException("", e);
		}
	}

	private static void validate(Map<Class<?>, Class<?>> narrowingMap) {
		Validate.notEmpty(narrowingMap, "");
		for (Class<?> parameterClass : narrowingMap.keySet()) {
			Class<?> narrowedParameterClass = narrowingMap.get(parameterClass);

			Validate.notNull(parameterClass, "");
			Validate.notNull(narrowedParameterClass, "");
			Validate.isTrue(!parameterClass.equals(narrowedParameterClass), "");
			Validate.isTrue(
					parameterClass.isAssignableFrom(narrowedParameterClass), "");
		}
	}

	@SuppressWarnings("unchecked")
	private Class<? extends T> castSafely(Class<?> derivedClass) {
		assert derivedClass.getSuperclass().equals(superClass);

		return (Class<? extends T>) derivedClass;
	}

	private static Map<Type, Type> buildParameterTypesNarrowingRules(
			Map<Class<?>, Class<?>> narrowingMap) {

		Map<Type, Type> parameterTypesNarrowingRules = new HashMap<Type, Type>();
		for (Map.Entry<Class<?>, Class<?>> entry : narrowingMap.entrySet()) {
			parameterTypesNarrowingRules.put(Type.getType(entry.getKey()),
					Type.getType(entry.getValue()));
		}

		return parameterTypesNarrowingRules;
	}

	private class ConstructorsParameterTypesNarrowingTransformer extends
			ClassEmitterTransformer {

		private final String derivedClassName;

		private final Map<Type, Type> parameterTypesNarrowingRules;

		public ConstructorsParameterTypesNarrowingTransformer(
				String derivedClassName,
				Map<Type, Type> parameterTypesNarrowingRules) {

			this.derivedClassName = derivedClassName;
			this.parameterTypesNarrowingRules = parameterTypesNarrowingRules;
		}

		@Override
		public void begin_class(int version, int access, String className,
				Type superType, Type[] interfaces, String source) {

			super.begin_class(version, access, derivedClassName,
					Type.getType(superClass), null, SOURCE_FILE);
		}

		@Override
		public CodeEmitter begin_method(int access, Signature signature,
				Type[] exceptions) {

			if (signature.getName().equals(CONSTRUCTOR_NAME)) {
				CodeEmitter codeEmitter = super.begin_method(access,
						rewriteConstructorSignature(signature), exceptions);

				codeEmitter.load_this();
				codeEmitter.load_args();
				codeEmitter.super_invoke_constructor(signature);
				codeEmitter.return_value();

				return new CodeEmitter(codeEmitter) {
					@Override
					public void visitEnd() {
						end_method();
					}
				};
			}

			return null;
		}

		private Signature rewriteConstructorSignature(Signature signature) {
			Type[] argumentTypes = signature.getArgumentTypes();
			for (int i = 0; i < argumentTypes.length; i++) {
				Type type = argumentTypes[i];
				if (parameterTypesNarrowingRules.containsKey(type)) {
					argumentTypes[i] = parameterTypesNarrowingRules.get(type);
				}
			}

			return new Signature(signature.getName(),
					signature.getReturnType(), argumentTypes);
		}

		@Override
		public void visitSource(String source, String debug) {
		}

		@Override
		public void visitOuterClass(String owner, String name, String descriptor) {
		}

		@Override
		public void visitInnerClass(String name, String outerName,
				String innerName, int access) {
		}

		@Override
		public FieldVisitor visitField(int access, String name,
				String descriptor, String signature, Object value) {

			return null;
		}
	}
}
